In [82]:
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import GetAssetsRequest
from alpaca.trading.enums import AssetClass


from alpaca.data.historical import CryptoHistoricalDataClient
from alpaca.data.requests import CryptoBarsRequest


from alpaca_secrets import APCA_API_KEY_ID, APCA_API_SECRET_KEY
import pandas as pd
import numpy as np
import talib

from backtesting import Strategy
from backtesting import Backtest, Strategy
from backtesting.lib import crossover

from datetime import datetime, timedelta
import inspect


trading_client = TradingClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY)

import multiprocessing as mp

mp.set_start_method("fork", force=True)

Data Collection¶

In [83]:
# search for US equities
search_params = GetAssetsRequest(asset_class=AssetClass.US_EQUITY)

assets = trading_client.get_all_assets(search_params)
In [84]:
def  prepare_data(list_symbol, n_years = 5):

    data_client = StockHistoricalDataClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY)

    end_date = datetime(2025,7,15)
    start_date = end_date - timedelta(days=n_years*365)

    bars_request = StockBarsRequest(
        symbol_or_symbols=list_symbol,
        timeframe=TimeFrame.Minute,
        # timeframe=TimeFrame.Hour,
        start=start_date,
        end=end_date,
        adjustment="all"

    )

    bars = data_client.get_stock_bars(bars_request).data

    dfs = {}
    for sym in list_symbol:
        print(f"Processing {sym}")

        try:
            asset = trading_client.get_asset(sym)
            print(f"{asset.symbol}: Tradable = {asset.tradable}")
        except Exception as e:
            print(f"{sym}: Error - {e}")


        candle = bars.get(sym, None)
        if candle is not None:
            dfs[sym] = pd.DataFrame([{k: getattr(bar, k) for k in ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'vwap']} for bar in candle])
        
        
            df = dfs[sym][['timestamp', 'open', 'high', 'low', 'close', 'volume']].copy()
            df.columns = ['Timestamp', 'Open', 'High', 'Low', 'Close', 'Volume']
            df['Timestamp'] = pd.to_datetime(df['Timestamp'])
            df.set_index('Timestamp', inplace=True)
            dfs[sym] = df
        
    return dfs
In [85]:
def prepare_crypto_data(list_symbol, n_years=1):
    client = CryptoHistoricalDataClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY)
    end = datetime(2025, 7, 15)
    start = end - timedelta(days=n_years*365)

    crypto_data = {}

    for symbol in list_symbol:
        print(f"Fetching {symbol}...")

        request = CryptoBarsRequest(
            symbol_or_symbols=symbol,
            start=start,
            end=end,
            timeframe=TimeFrame.Minute,
            # timeframe=TimeFrame.Hour,
            adjustment="all"
        )

        bars = client.get_crypto_bars(request).df
        if bars.empty:
            print(f"No data for {symbol}")
            continue

        df = bars[bars.index.get_level_values(0) == symbol].droplevel(0).copy()
        df.index.name = "timestamp"
        df = df.rename(columns=str.lower)
        df = df.reset_index()

        # Supondo que df tenha ['open', 'high', 'low', 'close', 'volume']
        df = df[['timestamp', 'open', 'high', 'low', 'close', 'volume']].copy()
        df.columns = ['Timestamp', 'Open', 'High', 'Low', 'Close', 'Volume']
        df['Timestamp'] = pd.to_datetime(df['Timestamp'])
        df.set_index('Timestamp', inplace=True)

        crypto_data[symbol] = df

    return crypto_data
In [86]:
def print_results(results):
    print(f"Return [%]:           {results['Return [%]']:.2f}")
    print(f"Buy & Hold Return [%]: {results['Buy & Hold Return [%]']:.2f}")
    print(f"Sharpe Ratio:         {results['Sharpe Ratio']:.2f}")
    print(f"# Trades:             {results['_trades'].shape[0]}")
    print(f"Win Rate:             {results['Win Rate [%]']:.2f}%")
    print(f"Max Drawdown [%]:     {results['Max. Drawdown [%]']:.2f}")
    print(f"Avg Trade Duration:   {results['Avg. Trade Duration']}")
    print(f"Best Trade [%]:       {results['Best Trade [%]']:.2f}")
    print(f"Worst Trade [%]:      {results['Worst Trade [%]']:.2f}")
    print("="*60)

ETFs Data¶

In [87]:
list_symbol_ = ["SPY","QQQ","IWM","DIA","XLF","XLK","GLD","IAU","TLT","HYG",]

etfs_close_data={}

for sym in list_symbol_:
    etfs_close_data[sym] = pd.read_csv(
        f"etfs_{sym.split()[0]}.csv", index_col=0, parse_dates=True)

# for download of data
# etfs_close_data = prepare_data(list_symbol_, n_years = 1)

etfs_close_data.keys()
Out[87]:
dict_keys(['SPY', 'QQQ', 'IWM', 'DIA', 'XLF', 'XLK', 'GLD', 'IAU', 'TLT', 'HYG'])

Equities Data¶

In [88]:
eqt_symbol_ = ["AAPL","MSFT","GOOG","META","TSLA"]

eqt_close_data={}

for sym in eqt_symbol_:
    eqt_close_data[sym] = pd.read_csv(
        f"eqt_{sym.split()[0]}.csv", index_col=0, parse_dates=True)

# for download of data
# eqt_close_data = prepare_data(list_symbol_, n_years = 1)

eqt_close_data.keys()
Out[88]:
dict_keys(['AAPL', 'MSFT', 'GOOG', 'META', 'TSLA'])

Crypto Data¶

In [89]:
crypto_symbols = ["BTC/USD", "ETH/USD", "SOL/USD", "XRP/USD"]

crypto_close_data={}

for sym in crypto_symbols:
    crypto_close_data[sym] = pd.read_csv(
        f"crypto_{sym.split('/')[0]}.csv", index_col=0, parse_dates=True)

# for download of data
# crypto_close_data = prepare_crypto_data(crypto_symbols, n_years=1)

crypto_close_data.keys()
Out[89]:
dict_keys(['BTC/USD', 'ETH/USD', 'SOL/USD', 'XRP/USD'])

Mean Reversion Strat¶

In [90]:
class ZScoreMeanReversion(Strategy):
    window = 50
    threshold = 1

    def init(self):
        close = self.data.Close
        self.ma = self.I(lambda x: pd.Series(x).rolling(self.window).mean(), close)
        self.std = self.I(lambda x: pd.Series(x).rolling(self.window).std(), close)

    def next(self):
        if len(self.ma) < 2:
            return

        z = (self.data.Close[-1] - self.ma[-1]) / self.std[-1]
        if z > self.threshold:
            self.position.close()
            self.sell(size =1)
        elif z < -self.threshold:
            self.position.close()
            self.buy(size =1)
In [ ]:
class ZScoreMeanReversion_StopLoss(Strategy):
    # window = 20
    # threshold = 1.25
    # stop_loss_pct = 0.015  # 1% stop loss
    window = 50
    threshold = 1
    stop_loss_pct = 0.01  # 1% stop loss

    def init(self):
        close = self.data.Close
        self.ma = self.I(lambda x: pd.Series(x).rolling(self.window).mean(), close, name='ma')
        self.std = self.I(lambda x: pd.Series(x).rolling(self.window).std(), close, name='std')
        self.z = self.I(lambda x, ma, std: (x - ma) / std, close, self.ma, self.std, name='z')

        self.entry_price = None

    def next(self):
        if len(self.ma) < 2:
            return

        z = self.z[-1]
        price = self.data.Close[-1]

        if not self.position:
            if z < -self.threshold:
                self.entry_price = price
                self.buy(size=100)
            elif z > self.threshold:
                self.entry_price = price
                self.sell(size=100)

        else:
            stop_loss_hit = False
            if self.position.is_long:
                stop_price = self.entry_price * (1 - self.stop_loss_pct)
                if price <= stop_price:
                    stop_loss_hit = True
                elif z > 0:
                    self.position.close()
            elif self.position.is_short:
                stop_price = self.entry_price * (1 + self.stop_loss_pct)
                if price >= stop_price:
                    stop_loss_hit = True
                elif z < 0:
                    self.position.close()

            if stop_loss_hit:
                self.position.close()
In [92]:
class ZScoreMeanReversionImproved(Strategy):
    window = 50
    threshold = 1
    stop_loss_pct = 0.01
    atr_window = 14
    cooldown_period = 5

    def init(self):
        close = self.data.Close

        self.ma = self.I(lambda x: pd.Series(x).rolling(self.window).mean(), close)
        self.std = self.I(lambda x: pd.Series(x).rolling(self.window).std(), close)
        self.z = self.I(lambda x, ma, std: (x - ma) / std, close, self.ma, self.std)

        self.atr = self.I(self.compute_atr, self.data.High, self.data.Low, self.data.Close)

        self.entry_price = None
        self.stop_triggered_at = -100

    def compute_atr(self, high, low, close):
        high = pd.Series(high)
        low = pd.Series(low)
        close = pd.Series(close)
        prev_close = close.shift(1)

        tr = pd.concat([
            high - low,
            (high - prev_close).abs(),
            (low - prev_close).abs()
        ], axis=1).max(axis=1)

        atr = tr.rolling(self.atr_window).mean()
        return atr.values

    def next(self):
        if len(self.ma) < 2:
            return

        i = len(self.data.Close) - 1
        price = self.data.Close[-1]
        z = self.z[-1]
        atr = self.atr[-1]

        if i - self.stop_triggered_at < self.cooldown_period:
            return

        # STOP LOSS
        if self.position:
            stop_price = self.entry_price * (1 - self.stop_loss_pct) if self.position.is_long else self.entry_price * (1 + self.stop_loss_pct)

            if self.position.is_long:
                if price >= self.ma[-1] or price <= stop_price:
                    self.position.close()
                    if price <= stop_price:
                        self.stop_triggered_at = i

            elif self.position.is_short:
                if price <= self.ma[-1] or price >= stop_price:
                    self.position.close()
                    if price >= stop_price:
                        self.stop_triggered_at = i

        if not self.position and atr < 0.02 * price:
            if z < -self.threshold:
                self.entry_price = price
                self.buy(size=1)
            elif z > self.threshold:
                self.entry_price = price
                self.sell(size=1)
In [93]:
class RSIMeanReversion(Strategy):
    rsi_window = 14
    lower_thresh = 30
    upper_thresh = 70

    def init(self):
        close = pd.Series(self.data.Close)
        self.rsi = self.I(self.calculate_rsi, close)

    def calculate_rsi(self, series):
        if len(series) < self.rsi_window:
            return pd.Series([np.nan] * len(series))

        delta = pd.Series(series).diff()
        up = delta.clip(lower=0)
        down = -delta.clip(upper=0)

        roll_up = up.ewm(span=self.rsi_window, adjust=False).mean()
        roll_down = down.ewm(span=self.rsi_window, adjust=False).mean()

        rs = roll_up / roll_down
        rsi = 100 - (100 / (1 + rs))
        return rsi.fillna(0)

    def next(self):
        if len(self.rsi) == 0 or np.isnan(self.rsi[-1]):
            return

        if self.rsi[-1] < self.lower_thresh:
            self.position.close()
            self.buy(size=1)
        elif self.rsi[-1] > self.upper_thresh:
            self.position.close()
            self.sell(size=1)
In [94]:
class OUProcessReversion(Strategy):
    window = 100
    entry_threshold = 1.5
    exit_threshold = 0.5

    def init(self):
        close = self.data.Close
        self.mean = self.I(lambda x: pd.Series(x).ewm(span=self.window).mean(), close)
        self.std = self.I(lambda x: pd.Series(x).ewm(span=self.window).std(), close)

    def next(self):
        z_score = (self.data.Close[-1] - self.mean[-1]) / self.std[-1]

        if self.position.is_long and abs(z_score) < self.exit_threshold:
            self.position.close()
        elif self.position.is_short and abs(z_score) < self.exit_threshold:
            self.position.close()
        elif z_score > self.entry_threshold:
            self.position.close()
            self.sell(size=1)
        elif z_score < -self.entry_threshold:
            self.position.close()
            self.buy(size=1)
In [95]:
class MRATStrategy(Strategy):
    short_window = 21
    long_window = 200
    z_threshold = 1.0

    def init(self):
        close = self.data.Close
        self.ma_short = self.I(lambda x: pd.Series(x).rolling(self.short_window).mean(), close, name='ma_short')
        self.ma_long = self.I(lambda x: pd.Series(x).rolling(self.long_window).mean(), close, name='ma_long')

    def next(self):
        if len(self.ma_short) < self.long_window or len(self.ma_long) < self.long_window:
            return

        # MRAT = MA21 / MA200
        mrat = self.ma_short[-1] / self.ma_long[-1]

        # Histórico de MRAT para cálculo de desvio
        hist_mrat = np.array(self.ma_short[-self.long_window:] / self.ma_long[-self.long_window:])
        mean_mrat = np.mean(hist_mrat)
        std_mrat = np.std(hist_mrat)

        z_score = (mrat - mean_mrat) / std_mrat

        if abs(z_score) < 0.3 and self.position:
            self.position.close()

        # Long
        elif z_score > self.z_threshold:
            if self.position.is_short:
                self.position.close()
            if not self.position.is_long:
                self.buy(size=1)

        # Short
        elif z_score < -self.z_threshold:
            if self.position.is_long:
                self.position.close()
            if not self.position.is_short:
                self.sell(size=1)

Backtesting¶

In [96]:
def run_strategies(df, strategies, commission = 0.02, plots=False):
    df_bt = df.reset_index()[['Timestamp', 'Open', 'High', 'Low', 'Close', 'Volume']].copy()
    df_bt['Timestamp'] = pd.to_datetime(df_bt['Timestamp'])
    df_bt.set_index('Timestamp', inplace=True)

    for strategy in strategies:
        print("="*60)
        print(f"Running strategy: {strategy.__name__}")
        print("-"*60)
        
        bt = Backtest(df_bt, strategy, cash=100_000, commission=.01, exclusive_orders=True)
        results = bt.run()

        if plots:
            bt.plot()

        if 'all_results' not in locals():
            all_results = []

        result_dict = {
            "Strategy": strategy.__name__,
            "Return [%]": results['Return [%]'],
            "Buy & Hold Return [%]": results['Buy & Hold Return [%]'],
            "Sharpe Ratio": results['Sharpe Ratio'],
            "# Trades": results['_trades'].shape[0],
            "Win Rate [%]": results['Win Rate [%]'],
            "Max Drawdown [%]": results['Max. Drawdown [%]'],
            "Avg Trade Duration": results['Avg. Trade Duration'],
            "Best Trade [%]": results['Best Trade [%]'],
            "Worst Trade [%]": results['Worst Trade [%]'],
        }
        all_results.append(result_dict)

        # Após rodar todos, cria o DataFrame
    results_df = pd.DataFrame(all_results)
    return results_df
In [97]:
df = etfs_close_data["IAU"]
df

strategies = [ZScoreMeanReversion, ZScoreMeanReversion_StopLoss,ZScoreMeanReversionImproved,
RSIMeanReversion, OUProcessReversion, MRATStrategy]

all_results = run_strategies(df, strategies, commission =0)
============================================================
Running strategy: ZScoreMeanReversion
------------------------------------------------------------
============================================================
Running strategy: ZScoreMeanReversion_StopLoss
------------------------------------------------------------
============================================================
Running strategy: ZScoreMeanReversionImproved
------------------------------------------------------------
============================================================
Running strategy: RSIMeanReversion
------------------------------------------------------------
============================================================
Running strategy: OUProcessReversion
------------------------------------------------------------
============================================================
Running strategy: MRATStrategy
------------------------------------------------------------

Selection best strats on simple parameters¶

In [98]:
all_results.sort_values(by='Return [%]', ascending=False).head(3)
Out[98]:
Strategy Return [%] Buy & Hold Return [%] Sharpe Ratio # Trades Win Rate [%] Max Drawdown [%] Avg Trade Duration Best Trade [%] Worst Trade [%]
5 MRATStrategy -0.828277 37.095021 -31.034327 784 40.178571 -0.828407 0 days 08:30:00 3.525488 -1.811448
2 ZScoreMeanReversionImproved -4.331412 38.176638 -42.821752 3976 66.825956 -4.331412 0 days 01:43:00 2.063690 -2.066251
4 OUProcessReversion -13.072290 38.601891 -33.679329 11997 39.568225 -13.072290 0 days 00:21:00 2.067752 -2.737069
In [99]:
all_results.sort_values(by='Win Rate [%]', ascending=False).head(3)
Out[99]:
Strategy Return [%] Buy & Hold Return [%] Sharpe Ratio # Trades Win Rate [%] Max Drawdown [%] Avg Trade Duration Best Trade [%] Worst Trade [%]
1 ZScoreMeanReversion_StopLoss -90.460280 38.176638 -25.904086 947 68.743400 -90.460280 0 days 01:54:00 0.736377 -1.384920
2 ZScoreMeanReversionImproved -4.331412 38.176638 -42.821752 3976 66.825956 -4.331412 0 days 01:43:00 2.063690 -2.066251
5 MRATStrategy -0.828277 37.095021 -31.034327 784 40.178571 -0.828407 0 days 08:30:00 3.525488 -1.811448
In [100]:
all_results.sort_values(by='Sharpe Ratio', ascending=False).head(3)
Out[100]:
Strategy Return [%] Buy & Hold Return [%] Sharpe Ratio # Trades Win Rate [%] Max Drawdown [%] Avg Trade Duration Best Trade [%] Worst Trade [%]
1 ZScoreMeanReversion_StopLoss -90.460280 38.176638 -25.904086 947 68.743400 -90.460280 0 days 01:54:00 0.736377 -1.384920
5 MRATStrategy -0.828277 37.095021 -31.034327 784 40.178571 -0.828407 0 days 08:30:00 3.525488 -1.811448
4 OUProcessReversion -13.072290 38.601891 -33.679329 11997 39.568225 -13.072290 0 days 00:21:00 2.067752 -2.737069

optmizing parameters for each top strats¶

MRATStrategy¶

In [101]:
bt = Backtest(df, MRATStrategy, cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    short_window=range(10, 50, 10),
    long_window=range(100, 250, 25),
    z_threshold=[0.5, 0.75, 1.0, 1.5],
    maximize='Sharpe Ratio',
    constraint=lambda p: p.short_window < p.long_window
)

print(results._strategy)

print_results(results)

bt.plot()
MRATStrategy(short_window=20,long_window=200,z_threshold=1.0)
Return [%]:           0.02
Buy & Hold Return [%]: 37.10
Sharpe Ratio:         2.99
# Trades:             795
Win Rate:             40.88%
Max Drawdown [%]:     -0.00
Avg Trade Duration:   0 days 08:18:00
Best Trade [%]:       3.53
Worst Trade [%]:      -1.81
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[101]:
GridPlot(
id = 'p9514', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p9217', ...), 0, 0), (figure(id='p9317', ...), 1, 0), (figure(id='p9170', ...), 2, 0), (figure(id='p9375', ...), 3, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p9513', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')
In [102]:
bt = Backtest(df, MRATStrategy, cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    short_window=range(10, 50, 10),
    long_window=range(100, 250, 25),
    z_threshold=[0.5, 0.75, 1.0, 1.5],
    maximize='Win Rate [%]',
    constraint=lambda p: p.short_window < p.long_window
)

print(results._strategy)

print_results(results)

bt.plot()
MRATStrategy(short_window=40,long_window=100,z_threshold=0.5)
Return [%]:           0.01
Buy & Hold Return [%]: 37.72
Sharpe Ratio:         1.58
# Trades:             1498
Win Rate:             45.33%
Max Drawdown [%]:     -0.01
Avg Trade Duration:   0 days 05:00:00
Best Trade [%]:       3.59
Worst Trade [%]:      -1.77
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[102]:
GridPlot(
id = 'p9891', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p9594', ...), 0, 0), (figure(id='p9694', ...), 1, 0), (figure(id='p9547', ...), 2, 0), (figure(id='p9752', ...), 3, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p9890', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')

OUProcessReversion¶

In [103]:
bt = Backtest(df, OUProcessReversion, cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    window=range(20, 200, 20),
    entry_threshold=[1.0, 1.25, 1.5, 1.75, 2.0],
    exit_threshold=[0.25, 0.5, 0.75, 1.0],
    maximize='Sharpe Ratio'
)
print(results._strategy)

print_results(results)

bt.plot()
OUProcessReversion(window=20,entry_threshold=1.25,exit_threshold=0.25)
Return [%]:           0.01
Buy & Hold Return [%]: 38.60
Sharpe Ratio:         1.48
# Trades:             19061
Win Rate:             40.01%
Max Drawdown [%]:     -0.00
Avg Trade Duration:   0 days 00:18:00
Best Trade [%]:       2.70
Worst Trade [%]:      -1.64
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[103]:
GridPlot(
id = 'p10308', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p9971', ...), 0, 0), (figure(id='p10071', ...), 1, 0), (figure(id='p9924', ...), 2, 0), (figure(id='p10129', ...), 3, 0), (figure(id='p10238', ...), 4, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p10307', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')
In [104]:
bt = Backtest(df, OUProcessReversion, cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    window=range(20, 200, 20),
    entry_threshold=[1.0, 1.25, 1.5, 1.75, 2.0],
    exit_threshold=[0.25, 0.5, 0.75, 1.0],
    maximize='Win Rate [%]',
)

print(results._strategy)

print_results(results)

bt.plot()
OUProcessReversion(window=20,entry_threshold=2.0,exit_threshold=0.25)
Return [%]:           0.00
Buy & Hold Return [%]: 38.60
Sharpe Ratio:         0.59
# Trades:             1250
Win Rate:             51.68%
Max Drawdown [%]:     -0.01
Avg Trade Duration:   0 days 02:21:00
Best Trade [%]:       1.21
Worst Trade [%]:      -2.65
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[104]:
GridPlot(
id = 'p10725', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p10388', ...), 0, 0), (figure(id='p10488', ...), 1, 0), (figure(id='p10341', ...), 2, 0), (figure(id='p10546', ...), 3, 0), (figure(id='p10655', ...), 4, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p10724', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')

ZScoreMeanReversion_StopLoss¶

In [ ]:
bt = Backtest(df, ZScoreMeanReversion_StopLoss, cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    window=range(20, 120, 10),
    threshold=[0.75, 1.0, 1.25, 1.5, 2.0],
    stop_loss_pct=[0.005, 0.01, 0.015, 0.02],
    maximize='Sharpe Ratio'
)

print(results._strategy)

print_results(results)

bt.plot()
ZScoreMeanReversion_StopLoss(window=20,threshold=1.25,stop_loss_pct=0.015)
Return [%]:           1.43
Buy & Hold Return [%]: 37.98
Sharpe Ratio:         2.10
# Trades:             7234
Win Rate:             63.84%
Max Drawdown [%]:     -0.52
Avg Trade Duration:   0 days 00:52:00
Best Trade [%]:       2.70
Worst Trade [%]:      -1.59
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[ ]:
GridPlot(
id = 'p5607', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p5221', ...), 0, 0), (figure(id='p5321', ...), 1, 0), (figure(id='p5174', ...), 2, 0), (figure(id='p5379', ...), 3, 0), (figure(id='p5488', ...), 4, 0), (figure(id='p5537', ...), 5, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p5606', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')
In [ ]:
bt = Backtest(df, ZScoreMeanReversion_StopLoss, cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    window=range(20, 120, 10),
    threshold=[0.75, 1.0, 1.25, 1.5, 2.0],
    stop_loss_pct=[0.005, 0.01, 0.015, 0.02],
    maximize='Sharpe Ratio',
)
print(results._strategy)

print_results(results)

bt.plot()
ZScoreMeanReversion_StopLoss(window=20,threshold=1.25,stop_loss_pct=0.015)
Return [%]:           1.43
Buy & Hold Return [%]: 37.98
Sharpe Ratio:         2.10
# Trades:             7234
Win Rate:             63.84%
Max Drawdown [%]:     -0.52
Avg Trade Duration:   0 days 00:52:00
Best Trade [%]:       2.70
Worst Trade [%]:      -1.59
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[ ]:
GridPlot(
id = 'p6074', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p5688', ...), 0, 0), (figure(id='p5788', ...), 1, 0), (figure(id='p5641', ...), 2, 0), (figure(id='p5846', ...), 3, 0), (figure(id='p5955', ...), 4, 0), (figure(id='p6004', ...), 5, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p6073', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')

ZScoreMeanReversionImproved¶

In [ ]:
bt = Backtest(df, ZScoreMeanReversionImproved, cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    window=range(20, 120, 10),
    threshold=[0.75, 1.0, 1.25, 1.5, 2.0],
    stop_loss_pct=[0.005, 0.01, 0.015, 0.02],
    maximize='Sharpe Ratio'
)

print(results._strategy)

print_results(results)

bt.plot()
ZScoreMeanReversionImproved(window=20,threshold=1.25,stop_loss_pct=0.01)
Return [%]:           0.15
Buy & Hold Return [%]: 37.98
Sharpe Ratio:         2.24
# Trades:             7252
Win Rate:             63.87%
Max Drawdown [%]:     -0.05
Avg Trade Duration:   0 days 00:51:00
Best Trade [%]:       2.70
Worst Trade [%]:      -1.41
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[ ]:
GridPlot(
id = 'p6591', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p6155', ...), 0, 0), (figure(id='p6255', ...), 1, 0), (figure(id='p6108', ...), 2, 0), (figure(id='p6313', ...), 3, 0), (figure(id='p6422', ...), 4, 0), (figure(id='p6471', ...), 5, 0), (figure(id='p6519', ...), 6, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p6590', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')
In [ ]:
bt = Backtest(df, ZScoreMeanReversionImproved, cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    window=range(20, 120, 10),
    threshold=[0.75, 1.0, 1.25, 1.5, 2.0],
    stop_loss_pct=[0.005, 0.01, 0.015, 0.02],
    maximize='Sharpe Ratio',
)
print(results._strategy)

print_results(results)

bt.plot()
ZScoreMeanReversionImproved(window=20,threshold=1.25,stop_loss_pct=0.01)
Return [%]:           0.15
Buy & Hold Return [%]: 37.98
Sharpe Ratio:         2.24
# Trades:             7252
Win Rate:             63.87%
Max Drawdown [%]:     -0.05
Avg Trade Duration:   0 days 00:51:00
Best Trade [%]:       2.70
Worst Trade [%]:      -1.41
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[ ]:
GridPlot(
id = 'p7109', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p6673', ...), 0, 0), (figure(id='p6773', ...), 1, 0), (figure(id='p6626', ...), 2, 0), (figure(id='p6831', ...), 3, 0), (figure(id='p6940', ...), 4, 0), (figure(id='p6989', ...), 5, 0), (figure(id='p7037', ...), 6, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p7108', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')

Summary¶

In [113]:
data = [
    ["MRATStrategy", "short=20, long=200, z=1.0", 0.22, 37.10, 2.98, 795, 40.88, -0.04, "0 days 08:18:00", 3.53, -1.81],
    ["MRATStrategy", "short=40, long=100, z=0.5", 0.14, 37.72, 1.58, 1498, 45.33, -0.08, "0 days 05:00:00", 3.59, -1.77],
    ["OUProcessReversion", "window=20, entry=1.25, exit=0.25", 0.01, 38.60, 1.48, 19061, 40.01, -0.00, "0 days 00:18:00", 2.70, -1.64],
    ["OUProcessReversion", "window=20, entry=2.0, exit=0.25", 0.00, 38.60, 0.59, 1250, 51.68, -0.01, "0 days 02:21:00", 1.21, -2.65],
    ["ZScoreMeanReversion_StopLoss", "window=20, threshold=1.25, sl=0.015", 1.43, 37.98, 2.10, 7234, 63.84, -0.52, "0 days 00:52:00", 2.70, -1.59],
    ["ZScoreMeanReversion_StopLoss", "window=20, threshold=1.25, sl=0.015", 1.43, 37.98, 2.10, 7234, 63.84, -0.52, "0 days 00:52:00", 2.70, -1.59],
    ["ZScoreMeanReversionImproved", "window=20, threshold=1.25, sl=0.01", 0.15, 37.98, 2.24, 7252, 63.87, -0.05, "0 days 00:51:00", 2.70, -1.41],
    ["ZScoreMeanReversionImproved", "window=20, threshold=1.25, sl=0.01", 0.15, 37.98, 2.24, 7252, 63.87, -0.05, "0 days 00:51:00", 2.70, -1.41]
]

columns = [
    "Strategy", "Params", "Return [%]", "Buy & Hold Return [%]", "Sharpe Ratio",
    "# Trades", "Win Rate [%]", "Max Drawdown [%]", "Avg Trade Duration",
    "Best Trade [%]", "Worst Trade [%]"
]

summary = pd.DataFrame(data, columns=columns)
summary
Out[113]:
Strategy Params Return [%] Buy & Hold Return [%] Sharpe Ratio # Trades Win Rate [%] Max Drawdown [%] Avg Trade Duration Best Trade [%] Worst Trade [%]
0 MRATStrategy short=20, long=200, z=1.0 0.22 37.10 2.98 795 40.88 -0.04 0 days 08:18:00 3.53 -1.81
1 MRATStrategy short=40, long=100, z=0.5 0.14 37.72 1.58 1498 45.33 -0.08 0 days 05:00:00 3.59 -1.77
2 OUProcessReversion window=20, entry=1.25, exit=0.25 0.01 38.60 1.48 19061 40.01 -0.00 0 days 00:18:00 2.70 -1.64
3 OUProcessReversion window=20, entry=2.0, exit=0.25 0.00 38.60 0.59 1250 51.68 -0.01 0 days 02:21:00 1.21 -2.65
4 ZScoreMeanReversion_StopLoss window=20, threshold=1.25, sl=0.015 1.43 37.98 2.10 7234 63.84 -0.52 0 days 00:52:00 2.70 -1.59
5 ZScoreMeanReversion_StopLoss window=20, threshold=1.25, sl=0.015 1.43 37.98 2.10 7234 63.84 -0.52 0 days 00:52:00 2.70 -1.59
6 ZScoreMeanReversionImproved window=20, threshold=1.25, sl=0.01 0.15 37.98 2.24 7252 63.87 -0.05 0 days 00:51:00 2.70 -1.41
7 ZScoreMeanReversionImproved window=20, threshold=1.25, sl=0.01 0.15 37.98 2.24 7252 63.87 -0.05 0 days 00:51:00 2.70 -1.41
In [117]:
summary.to_csv("strat_mean_rev_summary.csv",index=False)

df_meanrev = pd.read_csv("strat_mean_rev_summary.csv")
df_meanrev.to_latex("results_meanrev.csv_tex", index=False, longtable=True, escape=False)
In [ ]:
bt = Backtest(df, ZScoreMeanReversion_StopLoss, cash=100_000, commission=.00, exclusive_orders=True)
results = bt.run()

print(results._strategy)

print_results(results)

bt.plot()
# bt.plot(filename='final_image_trend_tsla.html')
ZScoreMeanReversion_StopLoss
Return [%]:           1.43
Buy & Hold Return [%]: 37.98
Sharpe Ratio:         2.10
# Trades:             7234
Win Rate:             63.84%
Max Drawdown [%]:     -0.52
Avg Trade Duration:   0 days 00:52:00
Best Trade [%]:       2.70
Worst Trade [%]:      -1.59
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[ ]:
GridPlot(
id = 'p12064', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p11678', ...), 0, 0), (figure(id='p11778', ...), 1, 0), (figure(id='p11631', ...), 2, 0), (figure(id='p11836', ...), 3, 0), (figure(id='p11945', ...), 4, 0), (figure(id='p11994', ...), 5, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p12063', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')
In [ ]: